---
title: Implementing pull-to-refresh
version: 1
---

import { Link } from "/src/components/Link";
import { AutoSnippet, When } from "/src/components/CodeSnippet";
import activity from "./pull_to_refresh/activity";
import fetchActivity from "./pull_to_refresh/fetch_activity";
import displayActivity from "!!raw-loader!./pull_to_refresh/display_activity.dart";
import displayActivity2 from "!!raw-loader!./pull_to_refresh/display_activity2.dart";
import displayActivity3 from "!!raw-loader!./pull_to_refresh/display_activity3.dart";
import displayActivity4 from "!!raw-loader!./pull_to_refresh/display_activity4.dart";
import fullApp from "./pull_to_refresh/full_app";

Riverpod natively supports pull-to-refresh thanks to its declarative nature.

In general, pull-to-refreshes can be complex due as there are multiple
problems to solve:

- Upon first entering a page, we want to show a spinner.
  But during refresh, we want to show the refresh indicator instead.
  We shouldn't show both the refresh indicator _and_ spinner.
- While a refresh is pending, we want to show the previous data/error.
- We need to show the refresh indicator for as long as the refresh is happening.

Let's see how to solve this using Riverpod.  
For this, we will make a simple example which recommends a random activity to users.  
And doing a pull-to-refresh will trigger a new suggestion:

<img
  alt="A gif of the previously described application working"
  src="/img/how_to/pull_to_refresh/app.gif"
/>

## Making a bare-bones application.

Before implement a pull-to-refresh, we first need something to refresh.  
We can make a simple application which uses [Bored API](https://www.boredapi.com/)
to suggests a random activity to users.

First, let's define an `Activity` class:

<AutoSnippet {...activity} />

That class will be responsible for representing a suggested activity
in a type-safe manner, and handle JSON encoding/decoding.  
Using Freezed/json_serializable is not required, but it is recommended.

Now, we'll want to define a provider making a HTTP GET request to fetch
a single activity:

<AutoSnippet {...fetchActivity} />

We can now use this provider to display a random activity.  
For now, we will not handle the loading/error state, and simply
display the activity when available:

<AutoSnippet raw={displayActivity} />

## Adding `RefreshIndicator`

Now that we have a simple application, we can add a `RefreshIndicator` to it.  
That widget is an official Material widget responsible for displaying a refresh indicator
when the user pulls down the screen.

Using `RefreshIndicator` requires a scrollable surface. But so far, we don't have
any. We can fix that by using a `ListView`/`GridView`/`SingleChildScrollView`/etc:

<AutoSnippet raw={displayActivity2} />

Users can now pull down the screen. But our data isn't refreshed yet.

## Adding the refresh logic

When users pull down the screen, `RefreshIndicator` will invoke
the `onRefresh` callback. We can use that callback to refresh our data.
In there, we can use `ref.refresh` to refresh the provider of our choice.

**Note**: `onRefresh` is expected to return a `Future`.
And it is important for that future to complete when the refresh is done.

To obtain such a future, we can read our provider's `.future` property.
This will return a future which completes when our provider has resolved.

We can therefore update our `RefreshIndicator` to look like this:

<AutoSnippet raw={displayActivity3} />

## Showing a spinner only during initial load and handling errors.

At the moment, our UI does not handle the error/loading states.  
Instead the data magically pops up when the loading/refresh is done.

Let's change this by gracefully handling those states. There are two
cases:

- During the initial load, we want to show a full-screen spinner.
- During a refresh, we want to show the refresh indicator
  and the previous data/error.

Fortunately, when listening to an asynchronous provider in Riverpod,
Riverpod gives us an `AsyncValue`, which offers everything we need.

That `AsyncValue` can then be combined with Dart 3.0's pattern matching
as follows:

<AutoSnippet raw={displayActivity4} />

:::caution
We use `valueOrNull` here, as currently, using `value` throws
if in error/loading state.

Riverpod 3.0 will change this to have `value` behave like `valueOrNull`.
But for now, let's stick to `valueOrNull`.
:::

:::tip
Notice the usage of the `:final valueOrNull?` syntax in our pattern matching.
This syntax can be used only because `activityProvider` returns a non-nullable
`Activity`.

If your data can be `null`, you can instead use `AsyncValue(hasData: true, :final valueOrNull)`.
This will correctly handle cases where the data is `null`, at the cost of
a few extra characters.
:::

## Wrapping up: full application

Here is the combined source of everything we've covered so far:

<AutoSnippet {...fullApp} />
